<?php
defined('BASE') or exit('Access Denied!');

/**
 * Obullo Framework (c) 2009.
 *
 * PHP5 MVC Based Minimalist Software.
 * 
 * @package         obullo       
 * @author          obullo.com
 * @copyright       Ersin Guvenc (c) 2009.
 * @license         public
 * @since           Version 1.0
 * @filesource
 * @license
 */

// ------------------------------------------------------------------------

/**
 * Obullo Security Helpers
 *
 * @package     Obullo
 * @subpackage  Helpers
 * @category    Helpers
 * @author      Ersin Guvenc
 * @link        
 */

// --------------------------------------------------------------------
if( ! isset($_secur->_ity)) 
{
    $_secur = Ssc::instance();
    $_secur->_ity = new stdClass();
    $_secur->_ity->xss_hash            = ''; 

    /* never allowed, string replacement */
    $_secur->_ity->never_allowed_str   = array(
                                        'document.cookie'   => '[removed]',
                                        'document.write'    => '[removed]',
                                        '.parentNode'       => '[removed]',
                                        '.innerHTML'        => '[removed]',
                                        'window.location'   => '[removed]',
                                        '-moz-binding'      => '[removed]',
                                        '<!--'              => '&lt;!--',
                                        '-->'               => '--&gt;',
                                        '<![CDATA['         => '&lt;![CDATA['
                                        );
                                    
    /* never allowed, regex replacement */
    $_secur->_ity->never_allowed_regex = array(
                                        "javascript\s*:"            => '[removed]',
                                        "expression\s*(\(|&\#40;)"  => '[removed]', // CSS and IE
                                        "vbscript\s*:"              => '[removed]', // IE, surprise!
                                        "Redirect\s+302"            => '[removed]'
                                    );
}

/**
* XSS Clean
*
* Sanitizes data so that Cross Site Scripting Hacks can be
* prevented.  This function does a fair amount of work but
* it is extremely thorough, designed to prevent even the
* most obscure XSS attempts.  Nothing is ever 100% foolproof,
* of course, but I haven't been able to get anything passed
* the filter.
*
* Note: This function should only be used to deal with data
* upon submission.  It's not something that should
* be used for general runtime processing.
*
* This function was based in part on some code and ideas I
* got from Bitflux: http://blog.bitflux.ch/wiki/XSS_Prevention
*
* To help develop this script I used this great list of
* vulnerabilities along with a few other hacks I've
* harvested from examining vulnerabilities in other programs:
* http://ha.ckers.org/xss.html
*
* @access   public
* @param    string
* @return   string
*/
if( ! function_exists('xss_clean') ) 
{
    function xss_clean($str, $is_image = FALSE)
    {
        $secur = Ssc::instance();
        /*
        * Is the string an array?
        *
        */
        if (is_array($str))
        {
            while (list($key) = each($str))
            {
                $str[$key] = xss_clean($str[$key]);
            }

            return $str;
        }

        /*
        * Remove Invisible Characters
        */
        $str = _remove_invisible_characters($str);

        /*
        * Protect GET variables in URLs
        */

        // 901119URL5918AMP18930PROTECT8198

        $str = preg_replace('|\&([a-z\_0-9\-]+)\=([a-z\_0-9\-]+)|i', xss_hash()."\\1=\\2", $str);

        /*
        * Validate standard character entities
        *
        * Add a semicolon if missing.  We do this to enable
        * the conversion of entities to ASCII later.
        *
        */
        $str = preg_replace('#(&\#?[0-9a-z]{2,})([\x00-\x20])*;?#i', "\\1;\\2", $str);

        /*
        * Validate UTF16 two byte encoding (x00) 
        *
        * Just as above, adds a semicolon if missing.
        *
        */
        $str = preg_replace('#(&\#x?)([0-9A-F]+);?#i',"\\1\\2;",$str);

        /*
        * Un-Protect GET variables in URLs
        */
        $str = str_replace(xss_hash(), '&', $str);

        /*
        * URL Decode
        *
        * Just in case stuff like this is submitted:
        *
        * <a href="http://%77%77%77%2E%67%6F%6F%67%6C%65%2E%63%6F%6D">Google</a>
        *
        * Note: Use rawurldecode() so it does not remove plus signs
        *
        */
        $str = rawurldecode($str);

        /*
        * Convert character entities to ASCII 
        *
        * This permits our tests below to work reliably.
        * We only convert entities that are within tags since
        * these are the ones that will pose security problems.
        *
        */

        $str = preg_replace_callback("/[a-z]+=([\'\"]).*?\\1/si", '_convert_attribute', $str);
        $str = preg_replace_callback("/<\w+.*?(?=>|<|$)/si", '_html_entity_decode_callback', $str);

        /*
        * Remove Invisible Characters Again!
        */
        $str = _remove_invisible_characters($str);

        /*
        * Convert all tabs to spaces
        *
        * This prevents strings like this: ja    vascript
        * NOTE: we deal with spaces between characters later.
        * NOTE: preg_replace was found to be amazingly slow here on large blocks of data,
        * so we use str_replace.
        *
        */

        if (strpos($str, "\t") !== FALSE)
        {
            $str = str_replace("\t", ' ', $str);
        }

        /*
        * Capture converted string for later comparison
        */
        $converted_string = $str;

        /*
        * Not Allowed Under Any Conditions
        */

        foreach ($secur->_ity->never_allowed_str as $key => $val)
        {
            $str = str_replace($key, $val, $str);   
        }

        foreach ($secur->_ity->never_allowed_regex as $key => $val)
        {
            $str = preg_replace("#".$key."#i", $val, $str);   
        }

        /*
        * Makes PHP tags safe
        *
        *  Note: XML tags are inadvertently replaced too:
        *
        *    <?xml
        *
        * But it doesn't seem to pose a problem.
        *
        */
        if ($is_image === TRUE)
        {
            // Images have a tendency to have the PHP short opening and closing tags every so often
            // so we skip those and only do the long opening tags.
            $str = preg_replace('/<\?(php)/i', "&lt;?\\1", $str);
        }
        else
        {
            $str = str_replace(array('<?', '?'.'>'),  array('&lt;?', '?&gt;'), $str);
        }

        /*
        * Compact any exploded words
        *
        * This corrects words like:  j a v a s c r i p t
        * These words are compacted back to their correct state.
        *
        */
        $words = array('javascript', 'expression', 'vbscript', 'script', 'applet', 'alert', 'document', 'write', 'cookie', 'window');
        foreach ($words as $word)
        {
            $temp = '';

            for ($i = 0, $wordlen = strlen($word); $i < $wordlen; $i++)
            {
                $temp .= substr($word, $i, 1)."\s*";
            }

            // We only want to do this when it is followed by a non-word character
            // That way valid stuff like "dealer to" does not become "dealerto"
            $str = preg_replace_callback('#('.substr($temp, 0, -3).')(\W)#is', '_compact_exploded_words', $str);
        }

        /*
        * Remove disallowed Javascript in links or img tags
        * We used to do some version comparisons and use of stripos for PHP5, but it is dog slow compared
        * to these simplified non-capturing preg_match(), especially if the pattern exists in the string
        */
        do
        {
            $original = $str;

            if (preg_match("/<a/i", $str))
            {
                $str = preg_replace_callback("#<a\s+([^>]*?)(>|$)#si", '_js_link_removal', $str);
            }

            if (preg_match("/<img/i", $str))
            {
                $str = preg_replace_callback("#<img\s+([^>]*?)(\s?/?>|$)#si", '_js_img_removal', $str);
            }

            if (preg_match("/script/i", $str) OR preg_match("/xss/i", $str))
            {
                $str = preg_replace("#<(/*)(script|xss)(.*?)\>#si", '[removed]', $str);
            }
        }
        while($original != $str);

        unset($original);

        /*
        * Remove JavaScript Event Handlers
        *
        * Note: This code is a little blunt.  It removes
        * the event handler and anything up to the closing >,
        * but it's unlikely to be a problem.
        *
        */
        $event_handlers = array('[^a-z_\-]on\w*','xmlns');

        if ($is_image === TRUE)
        {
            /*
            * Adobe Photoshop puts XML metadata into JFIF images, including namespacing, 
            * so we have to allow this for images. -Paul
            */
            unset($event_handlers[array_search('xmlns', $event_handlers)]);
        }

        $str = preg_replace("#<([^><]+?)(".implode('|', $event_handlers).")(\s*=\s*[^><]*)([><]*)#i", "<\\1\\4", $str);

        /*
        * Sanitize naughty HTML elements
        *
        * If a tag containing any of the words in the list
        * below is found, the tag gets converted to entities.
        *
        * So this: <blink>
        * Becomes: &lt;blink&gt;
        *
        */
        $naughty = 'alert|applet|audio|basefont|base|behavior|bgsound|blink|body|embed|expression|form|frameset|frame|head|html|ilayer|iframe|input|isindex|layer|link|meta|object|plaintext|style|script|textarea|title|video|xml|xss';
        $str = preg_replace_callback('#<(/*\s*)('.$naughty.')([^><]*)([><]*)#is', '_sanitize_naughty_html', $str);

        /*
        * Sanitize naughty scripting elements
        *
        * Similar to above, only instead of looking for
        * tags it looks for PHP and JavaScript commands
        * that are disallowed.  Rather than removing the
        * code, it simply converts the parenthesis to entities
        * rendering the code un-executable.
        *
        * For example:    eval('some code')
        * Becomes:        eval&#40;'some code'&#41;
        *
        */
        $str = preg_replace('#(alert|cmd|passthru|eval|exec|expression|system|fopen|fsockopen|file|file_get_contents|readfile|unlink)(\s*)\((.*?)\)#si', "\\1\\2&#40;\\3&#41;", $str);

        /*
        * Final clean up
        *
        * This adds a bit of extra precaution in case
        * something got through the above filters
        *
        */
        foreach ($secur->_ity->never_allowed_str as $key => $val)
        {
            $str = str_replace($key, $val, $str);   
        }

        foreach ($secur->_ity->never_allowed_regex as $key => $val)
        {
            $str = preg_replace("#".$key."#i", $val, $str);
        }

        /*
        *  Images are Handled in a Special Way
        *  - Essentially, we want to know that after all of the character conversion is done whether
        *  any unwanted, likely XSS, code was found.  If not, we return TRUE, as the image is clean.
        *  However, if the string post-conversion does not matched the string post-removal of XSS,
        *  then it fails, as there was unwanted XSS code found and removed/changed during processing.
        */

        if ($is_image === TRUE)
        {
            if ($str == $converted_string)
            {
                return TRUE;
            }
            else
            {
                return FALSE;
            }
        }

        log_message('debug', "XSS Filtering completed");
        return $str;
    }
}
// --------------------------------------------------------------------

/**
* Random Hash for protecting URLs
* 
* @version   0.1 
* @version   0.2 Obullo changes removed php_version
* @access    public
* @return    string
*/
if( ! function_exists('xss_hash') ) 
{
    function xss_hash()
    {
        $secur = Ssc::instance();
        
        if ($secur->_ity->xss_hash == '')
        {
            mt_srand();
            
            $secur->_ity->xss_hash = md5(time() + mt_rand(0, 1999999999));
        }

        return $secur->_ity->xss_hash;
    }
}
// --------------------------------------------------------------------

/**
* Remove Invisible Characters
*
* This prevents sandwiching null characters
* between ascii characters, like Java\0script.
*
* @access   public
* @param    string
* @return   string
*/
if( ! function_exists('_remove_invisible_characters') ) 
{
    function _remove_invisible_characters($str)
    {
        static $non_displayables;

        if ( ! isset($non_displayables))
        {
            // every control character except newline (dec 10), carriage return (dec 13), and horizontal tab (dec 09),
            $non_displayables = array(
                                        '/%0[0-8bcef]/',            // url encoded 00-08, 11, 12, 14, 15
                                        '/%1[0-9a-f]/',             // url encoded 16-31
                                        '/[\x00-\x08]/',            // 00-08
                                        '/\x0b/', '/\x0c/',         // 11, 12
                                        '/[\x0e-\x1f]/'             // 14-31
                                    );
        }

        do
        {
            $cleaned = $str;
            $str = preg_replace($non_displayables, '', $str);
        }
        while ($cleaned != $str);

        return $str;
    }
}
// --------------------------------------------------------------------

/**
* Compact Exploded Words
*
* Callback function for xss_clean() to remove whitespace from
* things like j a v a s c r i p t
*
* @access   public
* @param    type
* @return   type
*/
if( ! function_exists('_compact_exploded_words') ) 
{
    function _compact_exploded_words($matches)
    {
        return preg_replace('/\s+/s', '', $matches[1]).$matches[2];
    }
}
// --------------------------------------------------------------------

/**
* Sanitize Naughty HTML
*
* Callback function for xss_clean() to remove naughty HTML elements
*
* @access   private
* @param    array
* @return   string
*/
if( ! function_exists('_sanitize_naughty_html') ) 
{
    function _sanitize_naughty_html($matches)
    {
        // encode opening brace
        $str = '&lt;'.$matches[1].$matches[2].$matches[3];

        // encode captured opening or closing brace to prevent recursive vectors
        $str .= str_replace(array('>', '<'), array('&gt;', '&lt;'), $matches[4]);

        return $str;
    }
}
// --------------------------------------------------------------------

/**
* JS Link Removal
*
* Callback function for xss_clean() to sanitize links
* This limits the PCRE backtracks, making it more performance friendly
* and prevents PREG_BACKTRACK_LIMIT_ERROR from being triggered in
* PHP 5.2+ on link-heavy strings
*
* @access    private
* @param    array
* @return    string
*/
if( ! function_exists('_js_link_removal') ) 
{
    function _js_link_removal($match)
    {
        $attributes = _filter_attributes(str_replace(array('<', '>'), '', $match[1]));
        return str_replace($match[1], preg_replace("#href=.*?(alert\(|alert&\#40;|javascript\:|charset\=|window\.|document\.|\.cookie|<script|<xss|base64\s*,)#si", "", $attributes), $match[0]);
    }
}

/**
* JS Image Removal
*
* Callback function for xss_clean() to sanitize image tags
* This limits the PCRE backtracks, making it more performance friendly
* and prevents PREG_BACKTRACK_LIMIT_ERROR from being triggered in
* PHP 5.2+ on image tag heavy strings
*
* @access   private
* @param    array
* @return   string
*/
if( ! function_exists('_js_img_removal') ) 
{
    function _js_img_removal($match)
    {
        $attributes = _filter_attributes(str_replace(array('<', '>'), '', $match[1]));
        return str_replace($match[1], preg_replace("#src=.*?(alert\(|alert&\#40;|javascript\:|charset\=|window\.|document\.|\.cookie|<script|<xss|base64\s*,)#si", "", $attributes), $match[0]);
    }
}
// --------------------------------------------------------------------

/**
* Attribute Conversion
*
* Used as a callback for XSS Clean
*
* @access   public
* @param    array
* @return   string
*/
if( ! function_exists('_convert_attribute') ) 
{
    function _convert_attribute($match)
    {
        return str_replace(array('>', '<', '\\'), array('&gt;', '&lt;', '\\\\'), $match[0]);
    }
}
// --------------------------------------------------------------------

/**
* HTML Entity Decode Callback
*
* Used as a callback for XSS Clean
*
* @access   public
* @param    array
* @return   string
*/
if( ! function_exists('_html_entity_decode_callback') ) 
{
    function _html_entity_decode_callback($match)
    {
        $config  = base_register('Config');
        $charset = $config->item('charset');

        return _html_entity_decode($match[0], strtoupper($charset));
    }
}
// --------------------------------------------------------------------

/**
* HTML Entities Decode
*
* This function is a replacement for html_entity_decode()
*
* In some versions of PHP the native function does not work
* when UTF-8 is the specified character set, so this gives us
* a work-around.  More info here:
* http://bugs.php.net/bug.php?id=25670
*
* @access   private
* @param    string
* @param    string
* @return   string
*/
/* -------------------------------------------------
/*  Replacement for html_entity_decode()
/* -------------------------------------------------*/

/*
NOTE: html_entity_decode() has a bug in some PHP versions when UTF-8 is the
character set, and the PHP developers said they were not back porting the
fix to versions other than PHP 5.x.
*/
if( ! function_exists('_html_entity_decode') ) 
{
    function _html_entity_decode($str, $charset='UTF-8')
    {
        if (stristr($str, '&') === FALSE) return $str;

        // The reason we are not using html_entity_decode() by itself is because
        // while it is not technically correct to leave out the semicolon
        // at the end of an entity most browsers will still interpret the entity
        // correctly.  html_entity_decode() does not convert entities without
        // semicolons, so we are left with our own little solution here. Bummer.

        if (function_exists('html_entity_decode') && (strtolower($charset) != 'utf-8' OR version_compare(phpversion(), '5.0.0', '>=')))
        {
            $str = html_entity_decode($str, ENT_COMPAT, $charset);
            $str = preg_replace('~&#x(0*[0-9a-f]{2,5})~ei', 'chr(hexdec("\\1"))', $str);
            return preg_replace('~&#([0-9]{2,4})~e', 'chr(\\1)', $str);
        }

        // Numeric Entities
        $str = preg_replace('~&#x(0*[0-9a-f]{2,5});{0,1}~ei', 'chr(hexdec("\\1"))', $str);
        $str = preg_replace('~&#([0-9]{2,4});{0,1}~e', 'chr(\\1)', $str);

        // Literal Entities - Slightly slow so we do another check
        if (stristr($str, '&') === FALSE)
        {
            $str = strtr($str, array_flip(get_html_translation_table(HTML_ENTITIES)));
        }

        return $str;
    }
}
// --------------------------------------------------------------------

/**
* Filter Attributes
*
* Filters tag attributes for consistency and safety
*
* @access   public
* @param    string
* @return   string
*/
if( ! function_exists('_filter_attributes') ) 
{
    function _filter_attributes($str)
    {
        $out = '';

        if (preg_match_all('#\s*[a-z\-]+\s*=\s*(\042|\047)([^\\1]*?)\\1#is', $str, $matches))
        {
            foreach ($matches[0] as $match)
            {
                $out .= preg_replace("#/\*.*?\*/#s", '', $match);
            }
        }

        return $out;
    }
}
// --------------------------------------------------------------------

/**
* Hash encode a string
*
* @access	public
* @param	string
* @return	string
*/
if( ! function_exists('do_hash') ) 
{
    function do_hash($str, $type = 'sha1')
    {
	    if ($type == 'sha1')
	    {
		    return sha1($str);
	    }
	    else
	    {
		    return md5($str);
	    }
    }
}
// ------------------------------------------------------------------------

/**
* Strip Image Tags
*
* @access	public
* @param	string
* @return	string
*/
if( ! function_exists('strip_image_tags') ) 
{
    function strip_image_tags($str)
    {
	    $str = preg_replace("#<img\s+.*?src\s*=\s*[\"'](.+?)[\"'].*?\>#", "\\1", $str);
	    $str = preg_replace("#<img\s+.*?src\s*=\s*(.+?).*?\>#", "\\1", $str);
		    
	    return $str;
    }
}
// ------------------------------------------------------------------------

/**
* Convert PHP tags to entities
*
* @access	public
* @param	string
* @return	string
*/
if( ! function_exists('encode_php_tags') ) 
{
    function encode_php_tags($str)
    {
	    return str_replace(array('<?php', '<?PHP', '<?', '?>'),  array('&lt;?php', '&lt;?PHP', '&lt;?', '?&gt;'), $str);
    }
}
/* End of file security.php */
/* Location: ./base/helpers/security.php */